import os
import random
import time

import pygame
from pygame.locals import *

import gummworld2

import context
import data
import camera_target
import tiledtmxloader
import sprite
from sprite import Sprite
import geometry
import wedge
import player
import zombie
import blood
import world
import settings
import loose
import win
import game_ui


class Spawner(object):

    def __init__(self, x, y, w, h, rate, initial, maximum, game):
        self.rect = pygame.Rect(x, y, w, h)
        self.rate = int(rate) # num zombies per minute
        self.timespan = 1.0 / self.rate * 60.0 # seconds
        self.initial = int(initial) 
        self.maximum = int(maximum)
        self.zombies = []
        self.now = 0
        self.next = self.now + self.timespan
        self.game = game
        for i in range(self.initial):
            self._make_zomby()
        
    def _make_zomby(self):
        if len(self.zombies) <= self.maximum:
            position = random.randrange(self.rect.left, self.rect.right), random.randrange(self.rect.top, self.rect.bottom)
            zomb = zombie.Zombie(position)
            self.zombies.append(zomb)
            self.game.zombies.append(zomb)
            self.game.world.add(zomb)
        
    def remove_zomby(self, zomb):
        if zomb in self.zombies:
            self.zombies.remove(zomb)
            
    def update(self, dt):
        self.now += dt
        if self.now >= self.next:
            self.next = self.now + self.timespan
            self._make_zomby()
        
class GameContext(context.Context):
    def __init__(self, map_name):
        print 'game.GameContext.init'
        
        # quick-access local copies, updated once per game loop
        gummworld2.State.screen = gummworld2.Screen(surface=pygame.display.get_surface())
        
        # the map and renderer
        map_name = data.filepath(os.path.join('map', map_name))
        gummworld2.State.map = gummworld2.toolkit.collapse_map(
            gummworld2.toolkit.load_tiled_tmx_map(map_name), (10,10))
        
        # attacking - 1 is melee, 3 is ranged
        self.mouse_clicks_1 = []
        self.mouse_clicks_3 = []
        self.mouse_pos = 0,0
        
        # And a world to unite them all. So they can fight to the death!
        tw,th = gummworld2.State.map.tile_size
        mw,mh = gummworld2.State.map.map_size
        width = tw * mw
        height = th * mh
        self.world = world.World((0,0,width,height))

        # make some zombies, aaagh!
        self.zombies = []
        self.walls = []
        self.doors = []
        self.get_objects()
        
        ## load the collision rects from the 'Collision' layer
        self.walls.extend(self.get_collision_rects_from_map())
        
        self.world.add_list(self.walls)
        self.world.add_list(self.doors)
        gummworld2.State.world = self.world
        
        # make the player
        self.player = player.Player((random.randrange(self.start.left, self.start.right), random.randrange(self.start.top, self.start.bottom)))
        self.world.add(self.player)
        
        gummworld2.State.camera = gummworld2.Camera(
            camera_target.CameraTarget(self.player.rect, self.player.position))
        gummworld2.State.clock = gummworld2.GameClock(
            ticks_per_second=25, max_fps=settings.fps)
        self.elapsed = 0.0
        
        # collision management
        self.bumped_zombies = []
        self.bumped_walls = []
        self.targets = []
        self.hitbox_points = None
        self.hitbox_gfx = None
        self.hitbox_expire = 0
        # game ui
        game_ui.HUD()
        game_ui.hud.add('playerhealth', game_ui.PlayerHealth(self.player.health))
        game_ui.hud.add('meleestatus', game_ui.WeaponStatus())
        game_ui.hud.add('rangedstatus', game_ui.WeaponStatus())
        
    def get_player_zones(self):
        target = None
        start = None
        world_map = gummworld2.State.map.tiled_map
        for obj_group in world_map.object_groups:
            goffx = obj_group.x
            goffy = obj_group.y
            for map_obj in obj_group.objects:
                w, h = (map_obj.width, map_obj.height)
                # print map_obj.name, map_obj.x, map_obj.y, size, goffx, goffy
                if map_obj.name == "target":
                    # print "target at", map_obj.x, map_obj.y, w, h
                    target = pygame.Rect(map_obj.x, map_obj.y, w, h)
                if map_obj.name == "start":
                    start =  pygame.Rect(map_obj.x, map_obj.y, w, h)
        assert start
        assert target
        return start, target

    def get_objects(self):
        objects = {}
        for group in gummworld2.State.map.tiled_map.object_groups:
            objects[group.name] = [(obj.name, obj.x, obj.y, obj.width, obj.height, obj) for obj in group.objects]
        self.start = pygame.Rect(*objects['start'][0][1:5])
        self.target = pygame.Rect(*objects['target'][0][1:5])
        try:
            self.walls = [geometry.RectGeometry(x,y,w,h) for (n,x,y,w,h,o) in objects['walls']]
            self.doors = [geometry.RectGeometry(x,y,w,h) for (n,x,y,w,h,o) in objects['doors']]
        except Exception, ex:
            print ex
        # TODO: correctly read settings for spawnpoints
        self.spawners = []
        for n,x,y,w,h,o in objects['spawn']:
            p = o.properties
            rate = p['rate'] if 'rate' in p else 1.
            initial = p['initial'] if 'initial' in p else w*h/500
            zmax = p['max'] if 'max' in p else initial * 2.
            self.spawners.append(Spawner(x,y,w,h,rate,initial,zmax,self))

    # TODO: remove this (obsolete)        
    def get_zomby_spawners(self):
        spawners = []
        world_map = gummworld2.State.map.tiled_map
        for obj_group in world_map.object_groups:
            goffx = obj_group.x
            goffy = obj_group.y
            for map_obj in obj_group.objects:
                w, h = (map_obj.width, map_obj.height)
                # print map_obj.name, map_obj.x, map_obj.y, size, goffx, goffy
                if map_obj.name == "zomby spawn":
                    # print "zomby spawn", int(map_obj.properties["rate"]), int(map_obj.properties["initial"]), int(map_obj.properties["max"])
                    spawner = Spawner(map_obj.x, map_obj.y, w, h, 
                                        int(map_obj.properties["rate"]), 
                                        int(map_obj.properties["initial"]), 
                                        int(map_obj.properties["max"]), self)
                    spawners.append(spawner)
        return spawners

    # TODO: remove this (obsolete)        
    def get_collision_rects_from_map(self):
        rects = []
        try:
            layer = gummworld2.State.map.tiled_map.named_layers['Collision']
            tilewidth = gummworld2.State.map.tiled_map.tilewidth
            tileheight = gummworld2.State.map.tiled_map.tileheight
            layer.visible = True
            for y in range(layer.height):
                for x in range(layer.width):
                    tile = layer.content2D[x][y]
                    if tile:
                        r = geometry.RectGeometry(x * tilewidth, y * tileheight, tilewidth, tileheight)
                        r.name = 'wall'
                        rects.append(r)
        except Exception, ex:
            print ex
        return rects

    def think(self):
        # update the context
        clock = gummworld2.State.clock
        self.elapsed += clock.tick()
        if clock.update_ready():
            self.get_input()
            self.update()
            self.elapsed = 0.0
        if clock.frame_ready():
            self.draw()
    
    def update(self):
        # update game logic
        self.update_player()
        self.update_camera()
        self.update_blood()
        self.update_zombies()
        self.update_spawners()
        self.update_ui()
        self.check_game_end_condition()
    
    def update_blood(self):
        blood.blood_sprites.update(self.elapsed)
    
    def update_spawners(self):
        for spawn in self.spawners:
            spawn.update(self.elapsed)

    def check_game_end_condition(self):
        if self.player.health <= 0:
            context.pop()
            context.push(loose.Loose())
        if self.player.rect.colliderect(self.target):
            context.pop()
            context.push(win.Win())
    
    def update_player(self):
        # we may need to back these out if we collide
        x,y = save_player = self.player.position
        
        # adjust camera according the keypresses
        dx = player.player.vx * self.elapsed
        dy = player.player.vy * self.elapsed
        x += dx
        y += dy
        
        # update player position
        dummy = gummworld2.model.QuadTreeObject( pygame.Rect(self.player.rect) )

        dummy.position = x,y
        self.world.add(dummy)
        
        # check collisions and react if necessary
        del self.bumped_zombies[:]
        del self.bumped_walls[:]
        for entity in self.world.get_collisions(dummy):
            if isinstance(entity, zombie.Zombie):
                self.bumped_zombies.append(entity)
            elif entity in self.walls:
                self.bumped_walls.append(entity)
        self.world.remove(dummy)
        if self.bumped_zombies or self.bumped_walls:
            if self.bumped_walls:
                x,y = save_player
                player_rect = self.player.rect.inflate(-1, -1)
                
                # Gummbum: DR0ID: you have to add a mini algorithm like this... 
                # Move (x,y). If you collide, undo move. 
                # Move (x,0). If you collide, undo move. 
                # Move(0,y). If you collide, undo move.
                player_rect.center = x + dx, y
                if player_rect.collidelist([w.rect for w in self.bumped_walls]) == -1:
                    # print "no collision in x"
                    x += dx
                player_rect.center = x, y + dy
                if player_rect.collidelist([w.rect for w in self.bumped_walls]) == -1:
                    # print "no collision in y"
                    y += dy
            self.player.position = x, y #self.player.position[0] + dx, self.player.position[1] + dy
        else:
            self.player.position = (x, y)
        
        ## check player attack
        def collide(poly, rect):
            geometry = gummworld2.geometry
            point_in_poly = geometry.point_in_poly
            for p in rect.topleft,rect.topright,rect.bottomright,rect.bottomleft:
                if point_in_poly(p, poly):
                    return True
            if geometry.poly_intersects_rect(poly, rect):
                return True
            return False
        if self.mouse_clicks_1:
            # MELEE ATTACK
            attack_mode = 'melee'
            mouse_clicks = self.mouse_clicks_1
            attack = self.player.melee_attack
            weapon = self.player.melee_weapon
        else:
            # RANGED ATTACK
            attack_mode = 'ranged'
            mouse_clicks = self.mouse_clicks_3
            attack = self.player.ranged_attack
            weapon = self.player.ranged_weapon
        del self.targets[:]
        if len(mouse_clicks) >= 2:
            # SET UP ATTACK
            def get_targets():
                targets = []
                hitbox = self.hitbox_points
                for sprite in self.world.entities_in(gummworld2.State.camera.rect):
                    if isinstance(sprite, zombie.Zombie):
                        if collide(hitbox, sprite.rect):
                            targets.append(sprite)
                return targets
            def min_distance(left, right):
                origin = self.player.rect.center
                d1 = int(gummworld2.geometry.distance(origin, left.rect.center))
                d2 = int(gummworld2.geometry.distance(origin, right.rect.center))
                return d1 if d1 < d2 else d2
            start = mouse_clicks[0]
            end = mouse_clicks[1]
            if end - start < settings.double_click:
                attack_power = 'default'
                magnitude = .5
            else:
                attack_power = 'special'
                magnitude = 1.
            # DEFAULT ATTACK
            self.update_hitboxes(weapon, attack_power)
            self.targets = get_targets()
            self.targets.sort(min_distance)
            self.targets = attack(self.targets, attack_power)
            if self.targets:
                # Splatter blood, only the ones that were hit
                player_pos = self.player.position
                angle_of = gummworld2.geometry.angle_of
                for s in self.targets:
                    zombie_pos = s.x,s.y
                    angle = 360 - angle_of(player_pos, zombie_pos)
                    blood.Blood(zombie_pos, angle, magnitude)
            del mouse_clicks[:]
        
        if time.time() > self.hitbox_expire:
            self.hitbox_gfx = None
        self.player.facing = gummworld2.geometry.angle_of(
            gummworld2.State.camera.steady_target_position, self.mouse_pos)
        self.player.think(self.elapsed)

    def update_hitboxes(self, weapon, attack_power):
        camera = gummworld2.State.camera
        world_to_screen = camera.world_to_screen
        screen_to_world = camera.screen_to_world
        #
        player_pos = camera.steady_target_position
        mouse_pos = self.mouse_pos
        player_pix = world_to_screen(player_pos)
        mouse_pix = world_to_screen(mouse_pos)
        angle = gummworld2.geometry.angle_of(player_pos, mouse_pos)
        ## compute hitbox
        radius = weapon.max_range
        pos = gummworld2.geometry.point_on_circumference(player_pos, radius, angle)
        arc = getattr(weapon, attack_power+'_arc')
        if arc == 360:
            hitbox = wedge.wedge(player_pos, mouse_pos, radius, arc, ndiv=9)
            gfx = wedge.wedge(player_pix, mouse_pix, radius, arc, ndiv=9)
        else:
            hitbox = wedge.wedge(player_pos, mouse_pos, radius, arc)
            gfx = wedge.wedge(player_pix, mouse_pix, radius, arc)
        ## save the important stuff
        self.hitbox_points = hitbox
        self.hitbox_gfx = gfx
        self.hitbox_expire = time.time() + .25
    
    def update_camera(self):
        gummworld2.State.camera.position = self.player.position
        self.world.add(gummworld2.State.camera.target)
        gummworld2.State.camera.update()
    
    def update_zombies(self):
        # please don't feed the zombies
        player_rect = self.player.rect
        bumped_zombies = self.bumped_zombies
        self.zombies[:] = []
        wall_rects = []
        for ent in self.world.entities_in(gummworld2.State.camera.rect):
            if isinstance(ent, zombie.Zombie):
                self.zombies.append(ent)
            elif ent in self.walls or ent in self.doors:
                wall_rects.append(ent.rect)
        for z in list(self.zombies):
            if z.active:
                if z in bumped_zombies:
                    z.nom_nom()
                # ## TO DO: zombie bumps wall?
                # rects = [wall_rects[idx] for idx in z.rect.collidelistall(wall_rects)]
                # if rects > -1:
                    # z.collide_wall(rects) # change direction randomly?
                # else:
                    # z.collide_wall(None) # change direction randomly?
                z.think(self.elapsed, wall_rects)

                if z.vx or z.vy:
                    self.world.add(z)
            else:
                self.remove_zomby(z)
    
    def update_ui(self):
        game_ui.hud.update('playerhealth', self.player.health)
        game_ui.hud.update('meleestatus', self.player.melee_weapon)
        game_ui.hud.update('rangedstatus', self.player.ranged_weapon)
    
    def remove_zomby(self, zomby):
        for spawn in self.spawners:
            spawn.remove_zomby(zomby)
        self.zombies.remove(zomby)
        self.world.remove(zomby)

    def draw(self):
        # draw the scene
        gummworld2.State.camera.interpolate()
        gummworld2.State.screen.clear()
        self.draw_map()
        self.draw_blood()
        self.draw_zombies()
        self.draw_player()
        if settings.printfps:
            draw_fps()
        self.draw_ui()
        gummworld2.State.screen.flip()

    def draw_map(self):
        # draw the visible map tiles
        gummworld2.toolkit.draw_tiles()
    
    def draw_blood(self):
        draw = gummworld2.toolkit.draw_sprite
        for b in blood.blood_sprites:
            draw(b)
    
    def draw_zombies(self):
        draw = gummworld2.toolkit.draw_sprite
        for z in self.zombies:
            draw(z)
    
    def draw_player(self):
        camera = gummworld2.State.camera
        screen = camera.surface
        x,y = camera.world_to_screen(camera.steady_target_position)
        blit_rect = self.player.blit_rect
        blit_rect.center = x,y
        camera.view.blit(self.player.image, blit_rect.topleft)
        ## draw target boxes
        if self.hitbox_gfx:
            pygame.draw.polygon(screen, (0,255,0,5), self.hitbox_gfx, 1)

    def draw_ui(self):
        screen = gummworld2.State.camera.view.surface
        game_ui.hud.draw(screen)
    
    def get_input(self):
        for e in pygame.event.get():
            if e.type == QUIT:
                context.pop()
            elif e.type == KEYDOWN:
                if e.key == K_ESCAPE: context.pop()
                elif e.key == K_TAB: self.player.equip_next_weapon('melee')
                elif e.key == K_LSHIFT: self.player.equip_next_weapon('ranged')
            elif e.type == MOUSEBUTTONDOWN:
                if e.button == 1: self.mouse_clicks_1.append(time.time())
                elif e.button == 3: self.mouse_clicks_3.append(time.time())
                elif e.button == 4: self.player.equip_next_weapon('ranged')
                elif e.button == 5: self.player.equip_next_weapon('melee')
            elif e.type == MOUSEBUTTONUP:
                if e.button == 1: self.mouse_clicks_1.append(time.time())
                elif e.button == 3: self.mouse_clicks_3.append(time.time())

        m = pygame.key.get_pressed()
        player.player.setmotion(m)
        
        mouse_pos = pygame.mouse.get_pos()
        self.mouse_pos = gummworld2.State.camera.screen_to_world(mouse_pos)

    def load_map(self, name, map_filename):
        map_parser = tiledtmxloader.TileMapParser()
        map = map_parser.parse_decode(map_filename)
        map.load(tiledtmxloader.ImageLoaderPygame())
        return map
    
def draw_fps(ftimes = [], font = []):
    if not font:
        font.append(pygame.font.Font(None, 20))
    tnow = pygame.time.get_ticks()
    ftimes.append(tnow)
    while ftimes[0] < tnow - 2000:
        del ftimes[0]
    if tnow - ftimes[0] <= 0:
        return
    fps = 1000. * (len(ftimes) - 1) / (tnow - ftimes[0])
    i = font[0].render("%.1ffps" % fps, True, Color('black'), Color('white'))
    r = i.get_rect()
    screen = pygame.display.get_surface()
    r.bottomleft = screen.get_rect().bottomleft
    screen.blit(i, r)



